feat(swift-sdk): deleteWallet wipes full wallet footprint#3653
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughImplements end-to-end wallet deletion: Rust FFI and manager removal that unregisters identities, identity-sync early-exit on unregistered identities, Swift persistence APIs to query and delete wallet-scoped data, keychain sweeping, a public Swift deleteWallet API, UI wiring, and tests. ChangesWallet Deletion
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ DashSDKFFI.xcframework built for this PR.
SwiftPM (host the zip at a stable URL, then use): .binaryTarget(
name: "DashSDKFFI",
url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
checksum: "1db9d73627f5f3fe50437d877ddfebe365f41f00adb5e0ece076e8769c40b6eb"
)Xcode manual integration:
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/rs-platform-wallet/src/manager/identity_sync.rs (1)
570-587:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid calling external persistence while holding
statewrite lock.
self.persister.store(...)runs underself.state.write(). Because this is an external call, it can stall or re-enter paths that needstate, blockingregister/unregister/update_watched_tokensand creating deadlock risk.Take a snapshot under lock, release lock before
store, then reacquire and re-check identity before applying cache updates.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/rs-platform-wallet/src/manager/identity_sync.rs` around lines 570 - 587, Currently persister.store(...) is called while holding the self.state.write() lock which can deadlock; instead, inside identity_sync.rs (around the code using state.get(&identity_id), existing_row, sentinel, and cs) clone or take a snapshot of the changeset (cs) and any needed existing_row data while holding the write lock, then drop the lock before calling self.persister.store(sentinel, cs_snapshot), and after store returns reacquire the write lock and re-check that the identity still exists (state.get(&identity_id)) before applying any cache updates or mutations; ensure you handle and log persister.store errors the same way but performed outside the lock.
🧹 Nitpick comments (2)
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift (2)
149-191: ⚡ Quick winAdd a non-matching wallet control for the wallet-scoped key sweep.
This only proves that the targeted
walletIdrow is removed. BecausedeleteAllIdentityPrivateKeys(forWalletId:)enumerates the entire service, a broken filter that deletes everyidentity_privkey.*item would still satisfy this test. Seed a second row with a differentwalletIdand assert it survives.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift` around lines 149 - 191, The test testThrowingKeychainSweepsUseIsolatedService currently only inserts one identity private key so deleteAllIdentityPrivateKeys(forWalletId:) could accidentally delete all identity_privkey.* rows; insert a second identity private key via KeychainManager.storeIdentityPrivateKey with a different walletId (e.g., walletId2 and publicKey2) before calling manager.deleteAllIdentityPrivateKeys(forWalletId: walletId) and after the deletion assert that retrieveIdentityPrivateKey(publicKeyHex: publicKey2) still returns non-nil while the original publicKey is nil, ensuring the sweep is scoped to the targeted walletId.
38-120: ⚡ Quick winExercise a transaction that becomes orphaned because of the wallet cascade.
orphanTxstarts out orphaned before deletion, so this test still passes ifdeleteWalletData(walletId:)misses transactions that only become orphaned after wallet/account/TXO rows are removed. Please add one wallet-owned TXO/transaction pair and assert that the transaction is swept after the delete.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift` around lines 38 - 120, Add a transaction+TXO pair that is initially owned by the wallet and therefore removed by the wallet cascade: create a new PersistentTransaction named walletOwnedTx and a matching PersistentTxo named walletOwnedTxo that is associated with the wallet (set whichever field ties a txo to a wallet in your model—e.g., walletId or an address/ownership field) and append walletOwnedTxo to walletOwnedTx.outputs, insert both into the context alongside the existing objects before saving, then after calling PlatformWalletPersistenceHandler.deleteWalletData(walletId:) assert that the fetched PersistentTransaction list no longer contains walletOwnedTx (i.e., transactions count remains 1 and walletOwnedTx is gone). This ensures transactions that only become orphaned by the wallet cascade are swept.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/rs-platform-wallet-ffi/src/persistence.rs`:
- Around line 416-422: store() currently only checks the retired set once and
can persist changes if retire_wallet() runs concurrently, allowing stale writes
for just-retired wallets; modify the persistence path to coordinate retirement
and writes atomically by introducing a per-wallet lifecycle lock or state
machine (e.g., a per-wallet mutex/atomic state) and use it in both store() and
retire_wallet() so that store() acquires the wallet lock/state before running
the callback and verifies retirement state again (or aborts) before committing
pending merges; ensure you reference and protect the same retired set and the
code paths that examine changeset.wallet_metadata and perform pending merges so
no post-retire writes can be committed.
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 349-353: The deleteWallet(walletId:) flow calls
persistenceHandler?.identityIdsForWallet(...) and proceeds to call
KeychainManager.shared.deleteAllIdentityPrivateKeys(forWalletId:) even when
persistenceHandler is nil (e.g., configure(..., modelContainer: nil)), which can
leave private keys in Keychain; update deleteWallet(walletId:) to guard that
persistenceHandler is present and identityIds were resolved before performing
any destructive Keychain operations—either make persistenceHandler mandatory for
this path (throw an error if nil) or explicitly fail early when identityIds
cannot be retrieved, using the existing persistenceHandler and
identityIdsForWallet(...) check to decide whether to proceed with
KeychainManager.shared.deleteAllKeychainItems(...) and
deleteAllIdentityPrivateKeys(...).
---
Outside diff comments:
In `@packages/rs-platform-wallet/src/manager/identity_sync.rs`:
- Around line 570-587: Currently persister.store(...) is called while holding
the self.state.write() lock which can deadlock; instead, inside identity_sync.rs
(around the code using state.get(&identity_id), existing_row, sentinel, and cs)
clone or take a snapshot of the changeset (cs) and any needed existing_row data
while holding the write lock, then drop the lock before calling
self.persister.store(sentinel, cs_snapshot), and after store returns reacquire
the write lock and re-check that the identity still exists
(state.get(&identity_id)) before applying any cache updates or mutations; ensure
you handle and log persister.store errors the same way but performed outside the
lock.
---
Nitpick comments:
In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift`:
- Around line 149-191: The test testThrowingKeychainSweepsUseIsolatedService
currently only inserts one identity private key so
deleteAllIdentityPrivateKeys(forWalletId:) could accidentally delete all
identity_privkey.* rows; insert a second identity private key via
KeychainManager.storeIdentityPrivateKey with a different walletId (e.g.,
walletId2 and publicKey2) before calling
manager.deleteAllIdentityPrivateKeys(forWalletId: walletId) and after the
deletion assert that retrieveIdentityPrivateKey(publicKeyHex: publicKey2) still
returns non-nil while the original publicKey is nil, ensuring the sweep is
scoped to the targeted walletId.
- Around line 38-120: Add a transaction+TXO pair that is initially owned by the
wallet and therefore removed by the wallet cascade: create a new
PersistentTransaction named walletOwnedTx and a matching PersistentTxo named
walletOwnedTxo that is associated with the wallet (set whichever field ties a
txo to a wallet in your model—e.g., walletId or an address/ownership field) and
append walletOwnedTxo to walletOwnedTx.outputs, insert both into the context
alongside the existing objects before saving, then after calling
PlatformWalletPersistenceHandler.deleteWalletData(walletId:) assert that the
fetched PersistentTransaction list no longer contains walletOwnedTx (i.e.,
transactions count remains 1 and walletOwnedTx is gone). This ensures
transactions that only become orphaned by the wallet cascade are swept.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 755ba89b-4903-4bcd-a4b0-33c76ed16508
📒 Files selected for processing (11)
packages/rs-platform-wallet-ffi/src/manager.rspackages/rs-platform-wallet-ffi/src/persistence.rspackages/rs-platform-wallet/src/changeset/traits.rspackages/rs-platform-wallet/src/manager/identity_sync.rspackages/rs-platform-wallet/src/manager/wallet_lifecycle.rspackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swiftpackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swiftpackages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/KeyManagerTests.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift
Review GateCommit:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 350-361: The PlatformWalletManager is removing the wallet via
platform_wallet_manager_remove_wallet and updating wallets before retrieving
per-identity IDs, which can leave a partial state if
persistenceHandler.identityIdsForWallet(walletId:) later throws; move the try
persistenceHandler.identityIdsForWallet(walletId: walletId) call to occur before
calling platform_wallet_manager_remove_wallet and before
wallets.removeValue(forKey:), so identityIds are resolved (and any errors
surface) prior to the FFI removal and in-memory mutation in the removeWallet
flow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b3bbda40-bf4e-4618-8b7b-b6176b0cf78a
📒 Files selected for processing (1)
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:
- Around line 343-360: owned_identity_ids is currently captured under
self.wallet_manager.read().await and remove_wallet(wallet_id) is called later
under a separate write lock, causing a race where identities can be added
between locks; fix by acquiring the wallet_manager write lock
(self.wallet_manager.write().await) once, snapshot the identities (use the same
accessors: identity_manager.wallet_identities.get(wallet_id) and
IdentityGettersV0::id()), and then call remove_wallet(wallet_id) while still
holding that write lock so the snapshot and removal occur atomically; apply the
same pattern to the similar block at lines 373-377.
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 2071-2150: Both identityIdsForWallet and deleteWalletData can run
while a Rust changeset is open and thus see/modify a partial backgroundContext;
to fix, prevent these helpers from running during an open changeset by checking
the same changeset state/lock used by beginChangeset/endChangeset at the start
of each function (e.g. call or inline a small guard like ensureNoOpenChangeset()
or wait on the existing changeset mutex/condition) before executing the existing
onQueue block, using the same shared flag/lock that beginChangeset/endChangeset
manipulate so these methods either wait until the changeset is closed or fail
fast rather than reading/saving the shared backgroundContext mid-changeset
(refer to identityIdsForWallet, deleteWalletData, backgroundContext, onQueue,
beginChangeset, endChangeset).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2f25090f-9577-49c3-8779-109d0e730aaf
📒 Files selected for processing (4)
packages/rs-platform-wallet-ffi/src/persistence.rspackages/rs-platform-wallet/src/manager/wallet_lifecycle.rspackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift
✅ Files skipped from review due to trivial changes (1)
- packages/rs-platform-wallet-ffi/src/persistence.rs
… footprint Adds `PlatformWalletManager.deleteWallet(walletId:) throws` — one SDK call that wipes a wallet's complete footprint: Rust manager state, SwiftData rows (including orphans the `@Relationship` graph doesn't reach), per-identity Keychain entries, and the network sync state when no sibling wallet remains. Closes the data-leakage path where `modelContext.delete(wallet)` alone left orphan rows (`PersistentTransaction`, `PersistentPendingInput`, `PersistentTokenBalance`) on the same network for the next wallet to see — bug from dashwallet-ios that was also present in the SDK's example app. Rust: - New `platform_wallet_manager_remove_wallet` FFI, idempotent on missing wallets. - `PlatformWalletManager::remove_wallet` snapshots the wallet's identity ids and unregisters each from `IdentitySyncManager` so per-identity token sync stops. - `IdentitySyncManager::apply_fresh_balances` re-checks the live identity state under its write lock before persisting, dropping the race where a mid-sync unregister could still emit a stale token balance. Swift: - `PlatformWalletPersistenceHandler.deleteWalletData` cascades the schema's @relationship chain, sweeps `PersistentPendingInput` by walletId denorm, sweeps orphan `PersistentTransaction` rows after the cascade, drops the network sync state when the last wallet on the network is gone. - `ensureWalletRecord` is non-creating; `persistWalletMetadata` is the sole callback that materializes a `PersistentWallet` row. Other callbacks that arrive for a wallet whose row doesn't exist (post-deletion stale callbacks) silently drop. - `KeychainManager` adds `deleteAllKeychainItems(forIdentityId:)` covering both `privkey_*` and `specialkey_*` schemes, plus `deleteAllIdentityPrivateKeys(forWalletId:)` for the wallet- derived `identity_privkey.*` rows. Tests: four Swift unit tests against an in-memory `ModelContainer` covering cascade + orphan sweep, idempotency under double-call, network-sync conditional cleanup (both last-wallet and sibling- remains branches), and the three Keychain sweep schemes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ind-only `ensureWalletRecord` now does find-or-create again and is used only by `persistWalletMetadata`. `findWalletRecord` is the non-creating sibling used by `persistWalletChangeset`, `setWalletName`, and `persistAccount`, each `guard`ing on the result. Same external behavior; names match the bodies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the window where a stale `persistIdentityKeys` callback could re-derive from the still-present mnemonic and write fresh `identity_privkey.*` entries that the keychain sweep wouldn't see. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (2)
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift (1)
350-363:⚠️ Potential issue | 🟠 Major | ⚡ Quick winResolve identity IDs before the destructive Rust removal call.
Line 356 removes the wallet first, but Line 361 can still throw. That leaves a partial wipe path where per-identity keychain cleanup never runs.
💡 Suggested fix
+ let identityIds = try persistenceHandler.identityIdsForWallet(walletId: walletId) + try walletId.withUnsafeBytes { raw in guard let base = raw.baseAddress?.assumingMemoryBound(to: FFIByteTuple32.self) else { throw PlatformWalletError.nullPointer( "wallet_id buffer base address was nil" ) } try platform_wallet_manager_remove_wallet(handle, base).check() } wallets.removeValue(forKey: walletId) - - let identityIds = try persistenceHandler.identityIdsForWallet(walletId: walletId) try persistenceHandler.deleteWalletData(walletId: walletId)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift` around lines 350 - 363, The code currently removes the in-memory wallet and then calls persistence work, but it retrieves identity IDs after the destructive Rust call/wallet removal; fetch the identity IDs first via persistenceHandler.identityIdsForWallet(walletId:), then perform the platform_wallet_manager_remove_wallet call and persistenceHandler.deleteWalletData, and only after all operations succeed remove the entry from the wallets dictionary (wallets.removeValue(forKey: walletId)); in short, call identityIdsForWallet before platform_wallet_manager_remove_wallet (and ensure wallets.removeValue runs last) so per-identity cleanup can run even if later steps throw.packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift (1)
2063-2143:⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy liftBlock wallet-deletion helpers during an open changeset.
Line 2063 and Line 2076 can execute while
inChangeset == true, which means Line 2137 (save) / Line 2139 (rollback) can commit or discard another round’s partial callback writes.💡 Suggested fix
+ private func ensureNoOpenChangeset() throws { + guard !inChangeset else { + throw PlatformWalletError.invalidHandle( + "wallet deletion helpers cannot run during an open persistence changeset" + ) + } + } + public func identityIdsForWallet(walletId: Data) throws -> [Data] { try onQueue { + try ensureNoOpenChangeset() let descriptor = FetchDescriptor<PersistentWallet>( predicate: PersistentWallet.predicate(walletId: walletId) ) guard let walletRow = try backgroundContext.fetch(descriptor).first else { return [] } return walletRow.identities.map { $0.identityId } } } public func deleteWalletData(walletId: Data) throws { try onQueue { + try ensureNoOpenChangeset() do { // existing body... try backgroundContext.save() } catch { backgroundContext.rollback() throw error } } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift` around lines 2063 - 2143, Both identityIdsForWallet(walletId:) and deleteWalletData(walletId:) must not run while inChangeset == true; add an early guard at the start of their onQueue closures (e.g. guard !inChangeset else { throw PersistenceError.changesetOpen } or return/throw appropriate error) so the body never executes when a changeset is open, preventing save()/rollback() from touching another active changeset; update references in those functions (identityIdsForWallet and deleteWalletData) to check inChangeset before fetching/deleting and propagate a clear error (e.g. PersistenceError.changesetOpen) to callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 350-363: The code currently removes the in-memory wallet and then
calls persistence work, but it retrieves identity IDs after the destructive Rust
call/wallet removal; fetch the identity IDs first via
persistenceHandler.identityIdsForWallet(walletId:), then perform the
platform_wallet_manager_remove_wallet call and
persistenceHandler.deleteWalletData, and only after all operations succeed
remove the entry from the wallets dictionary (wallets.removeValue(forKey:
walletId)); in short, call identityIdsForWallet before
platform_wallet_manager_remove_wallet (and ensure wallets.removeValue runs last)
so per-identity cleanup can run even if later steps throw.
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 2063-2143: Both identityIdsForWallet(walletId:) and
deleteWalletData(walletId:) must not run while inChangeset == true; add an early
guard at the start of their onQueue closures (e.g. guard !inChangeset else {
throw PersistenceError.changesetOpen } or return/throw appropriate error) so the
body never executes when a changeset is open, preventing save()/rollback() from
touching another active changeset; update references in those functions
(identityIdsForWallet and deleteWalletData) to check inChangeset before
fetching/deleting and propagate a clear error (e.g.
PersistenceError.changesetOpen) to callers.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2a55c114-8ccd-4c70-ba05-4e14f7f407b3
📒 Files selected for processing (10)
packages/rs-platform-wallet-ffi/src/manager.rspackages/rs-platform-wallet-ffi/src/persistence.rspackages/rs-platform-wallet/src/manager/identity_sync.rspackages/rs-platform-wallet/src/manager/wallet_lifecycle.rspackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swiftpackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swiftpackages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/KeyManagerTests.swiftpackages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift
🚧 Files skipped from review as they are similar to previous changes (6)
- packages/rs-platform-wallet-ffi/src/persistence.rs
- packages/rs-platform-wallet-ffi/src/manager.rs
- packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
- packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
- packages/rs-platform-wallet/src/manager/identity_sync.rs
- packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift
- Swift: resolve identity ids before the FFI removal so a SwiftData fetch failure surfaces before the destructive ops run. - Rust: hold the wallet_manager write lock across the identity snapshot and the wallet removal so a concurrent identity update can't leak a registration past unregister_identity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The orphan-tx sweep ran on stale in-memory relationship arrays, so transactions whose TXOs had just been cascade-deleted (or direct-deleted) weren't detected as orphan. Two changes together fix it: - Direct-delete `PersistentTxo` rows by `walletId` denorm before the wallet cascade so floating TXOs with no `coreAddress` link are still reached. - Save after the destructive deletes and before the orphan-tx sweep so `tx.outputs` / `tx.inputs` / `tx.pendingInputs` reflect the committed state rather than the pending one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 350-366: The code deletes SwiftData wallet rows before sweeping
identity-scoped keychain items, which can leave a permanent partial wipe if
keychain deletion throws; to fix, preserve the identityIds and perform
KeychainManager.shared.deleteAllKeychainItems(forIdentityId:) for each identity
first, ensuring any thrown error aborts before removing persistence, then call
try persistenceHandler.deleteWalletData(walletId:) only after the per-identity
loop completes successfully (also keep wallets.removeValue(forKey: walletId) and
platform_wallet_manager_remove_wallet(handle, ...) ordering consistent), so move
the persistenceHandler.deleteWalletData call below the for identityIds loop and
let errors propagate to avoid inconsistent state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a25c82b6-2b16-4b7a-becc-e9239598f99d
📒 Files selected for processing (3)
packages/rs-platform-wallet/src/manager/wallet_lifecycle.rspackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swiftpackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
- packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
Issue being fixed or feature implemented
A wallet deletion through the SDK left orphan data behind: SwiftData rows the
@Relationshipcascade graph doesn't reach (PersistentTransaction,PersistentPendingInput,PersistentTokenBalance), in-memory Rust manager state, per-identity Keychain entries, and the shared network sync checkpoint. The next wallet created on the same network observed ghost rows from the deleted one. The same gap was present in the SDK's own example app — itsWalletDetailView.deleteWallet()carried aTODOacknowledging the missingPlatformWalletManagerremoval API.What was done?
Adds
PlatformWalletManager.deleteWallet(walletId:) throws, a single SDK call that wipes a wallet's complete footprint:platform_wallet_manager_remove_walletFFI drops the wallet from the in-memory map, snapshots its identities, and unregisters them fromIdentitySyncManagerso per-identity token-balance polling stops.PersistentWalletrow before writing.ensureWalletRecordcreates only insidepersistWalletMetadata— the first callback dispatched in any registration round; every other wallet-scoped callback usesfindWalletRecordand silently drops when the row is missing. Closes the race where Rust tasks already in flight when the delete fired could resurrect rows.IdentitySyncManager::apply_fresh_balancesre-checks the live state under its write lock before persisting, so a balance fetched mid-sync for a just-unregistered identity is dropped on the floor..nullify; this explicit path takes them down), sweepsPersistentPendingInputby walletId denorm, sweepsPersistentTransactionrows now reachable by nothing, sweepsPersistentTokenBalancefor the cascaded identity ids, and deletes the network sync state row only when no sibling wallet remains on that network.privkey_*+specialkey_*rows and wallet-derivedidentity_privkey.<path>rows, thenWalletStoragemetadata then mnemonic last (mnemonic-as-retry-anchor — if any earlier step fails, the pre-flight on retry still sees it).The example app's
WalletDetailViewbecomes the first consumer;dashwallet-iosfollows.How Has This Been Tested?
WalletDeletionTests(Swift, against in-memoryModelContainer): cascade + orphan-tx sweep, idempotency under double-call, network-sync conditional cleanup (both last-wallet and sibling-remains branches), and per-scheme Keychain sweeps with an isolated keychain service.cargo check -p platform-wallet,cargo clippy -p platform-wallet-fficlean.build_ios.sh --target sim --profile devsucceeds with-warnings-as-errors.WalletDeletionTestspass underxcodebuild test.Worth a simulator smoke pass before merging: create a wallet, sync a bit, delete it, create another on the same network, confirm StorageExplorerView shows zero ghosts for the deleted walletId across every
Persistent*table.Breaking Changes
KeychainManager.deleteAllPrivateKeys(for:)andKeychainManager.deleteAllIdentityPrivateKeys(forWalletId:)are nowthrows(were-> Boolpreviously, swallowing keychain errors). External consumers — includingdashwallet-ios— will need totrythe calls.Conventional-commit type is
feat, notfeat!, because the breaking surface is limited to two helper methods whose previousBoolreturn value didn't distinguish "nothing to delete" from "delete failed" — call sites that relied on the boolean were already silently wrong. Happy to retitle tofeat!if reviewers prefer.Checklist:
For repository code-owners and collaborators only
Summary by CodeRabbit
New Features
Behavioral Changes
Tests